前兩篇文篇討論了 thread pinning 的基本原理:透過 CPU affinity 限制 Thread 的執行範圍,降低 CPU migration 與其他 scheduling interference,進一步改善 latency predictability。
然而真正進入 production environment 後,遠比單純使用 pthread_setaffinity_np() 等 API 複雜得多。因為即使我們成功把 thread 綁定到某個特定的 CPU,也無法保證此 CPU 是乾淨的。Linux kernel、interrupt、softirq、kernel worker、SMT sibling,以及 NUMA memory placement 都可能影響最後結果。
因此,本篇的重點是建立一套完整的方法,用 Linux 嘗試進行實戰演練。
1. 最基本的 CPU Affinity —— taskset
Linux 提供的 taskset 是最容易開始測試 CPU affinity 的工具,我們可以使用這些指令來限制應用程式所使用的 CPU 核心:
# 限制應用程式僅在單一 CPU 6 執行
taskset -c 6 ./my_application
# 查看特定 process 的 affinity 配置
taskset -cp 12345
# 預期回應 (代表 process 僅能在 CPU 6 執行):
# pid 12345's current affinity list: 6
# 啟動應用程式並綁定多個 CPU
taskset -c 4,6,7 ./my_application
# 此時 scheduler 僅能在允許的 CPU 之間輪替選擇:allowed CPUs = {4,6,7}
2. Application 內部 Pin Thread
Process-level affinity 有時候還不夠。若想落實更細緻的 thread 資源分配,可以在 thread 建立後設定 affinity。
Application Threads
⠀├── Main Thread
⠀├── Network Threadㅤ⠀→ CPU 4
⠀├── Worker 1ㅤㅤㅤㅤ⠀→ CPU 6
⠀├── Worker 2ㅤㅤㅤㅤ⠀→ CPU 7
⠀└── Logging Threadㅤㅤ→ CPU 1
Linux pthread 常見做法是:
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(6, &cpuset);
pthread_setaffinity_np(
pthread_self(),
sizeof(cpuset),
&cpuset
);
如果需要更完整的錯誤處理,production code 應該檢查 API return value,並確認指定的 CPU 在目前系統 topology 中有效。這種方式最大的優點是可以把 CPU placement 直接寫進 application architecture,對 pipeline-style workload 特別有用。
理想的架構映射範例:
RX Thread → CPU 4
Decode Thread → CPU 6
Encode Thread → CPU 7
3. CPU Isolation —— 從「我去哪裡」變成「別人不要來」
如果只使用 affinity 將 critical thread 綁定到 CPU 6,該 CPU 依然可能被系統排程器分配其他一般行程,或因處理硬體中斷與 kernel threads 而受到干擾。因此,在 latency-sensitive system 中,可以進一步考慮 CPU isolation。概念上類似於下圖:
CPU 0-3⠀housekeeping⠀⠀一般系統工作則盡可能留在這
CPU 4-7⠀isolated⠀ ⠀⠀⠀⠀可負責 application 的 critical threads
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Thread A → CPU 4
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Thread B → CPU 6
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Thread C → CPU 7
現代 Linux 提供 isolcpus、nohz_full 等多種 CPU isolation 與 scheduler tuning 機制。實際可用參數會隨 kernel 版本與 distribution 而有所不同,因此不應把某一組 boot parameter 視為所有系統通用的固定答案。
核心原則比較重要:先理解 topology,再決定 isolation strategy。
4. IRQ Affinity —— 常被忽略的干擾來源
假設 CPU 6 是 critical CPU,但某張高速網卡仍然把大量 interrupts 丟到 CPU 6:
Application Thread → CPU 6
NIC
⠀├── IRQ → CPU 6
⠀├── IRQ → CPU 6
⠀└── IRQ → CPU 6
那麼 application 再怎麼 pin,都可能遇到 latency spikes。所以 high-performance networking system 通常還需要處理 IRQ affinity。
Linux 可以透過 /proc/irq/ 下的 affinity 設定來控制特定 interrupt 的 CPU placement。例如查看:cat /proc/irq/<IRQ>/smp_affinity_list
實際部署時,可以把 network IRQ 分配到 housekeeping CPUs,而讓 application critical CPUs 儘量減少不必要的 interrupt activity,這樣才能真正形成 separation:
NIC IRQ → CPU 0 / CPU 1
Application RX → CPU 6
5. 不要忘記 SoftIRQ
Networking workload 特別容易踩到這個問題。硬體 interrupt 並不是全部工作,Linux network stack 還涉及 softirq processing。如果 CPU isolation 只處理 application thread,而沒有考慮 kernel networking activity,結果可能與預期不同。這也是為什麼高效能網路系統通常需要從整條路徑一起分析,而不是只看 application thread。
NIC Interrupt (Network Interface Card)
↓
Hard IRQ (Top Half)
↓ ─ [Kernel Networking Activity]
SoftIRQ (Bottom Half)
↓
Network Stack (IP/TCP/UDP Processing)
↓
Application (Critical Thread)
6. NUMA —— Thread 與 Memory 要一起配置
在 dual-socket 或 multi-socket server 上,CPU pinning 後下一個問題通常就是 memory locality。可以使用 lscpu 查看 CPU topology,也可以用 numactl --hardware 查看 NUMA node 與 memory topology。
如同 Day 19 提到的觀念,如果 critical thread 在 CPU 0-15,那麼通常也希望它主要使用 NUMA node 0 memory。測試時可以使用以下指令,讓 CPU 與 memory placement 更一致:numactl --cpunodebind=0 --membind=0 ./my_application
但同樣要注意:NUMA policy 不應盲目套用。實際最佳配置取決於 application 的 memory access pattern。
7. 如何確認 Thread 真的被 Pin 住?
不能只相信 configuration,關鍵在於這三者之間的驗證:
┌────────────┐
│ Configuration │ NUMA / CPU affinity 設定
└─────┬──────┘
│── 檢驗配置邊界:taskset -cp <PID>
┌─────┴──────┐
│ Observed CPU Placement │ 透過工具觀察到的動態實際位置
└─────┬──────┘
│── 查看核心分配:ps -eLo pid,tid,psr,comm
┌─────┴──────┐
│ Measured Latency │ 最後量測出來的效能與延遲結果
└────────────┘
其中 PSR 可以用來觀察 thread 最近執行所在的 processor,更進一步可以使用 perf 或其他 tracing/profiling 工具觀察 scheduler behavior。